Este código forma parte del trabajo final del máster en ciencia de datos; en él se van a poner en práctica todos los conocimientos adquiridos en las distintas asignaturas. Como se indica en el título 'Detección de transacciones fraudulentas con tarjetas bancarias usando técnicas de clasificación', el objetivo es conseguir un modelo que sea capaz de clasificar transacciones bancarias en fraudulentas o no fraudulentas, así como poder conocer cuáles son las variables que más ayudan a la clasificación.
Para ello, voy a llevar a cabo todas las fases de un proyecto de Machine Learning: análisis y limpieza de los datos, transformaciones, selección variables y modelado. En la fase de modelado, contruiré distintos modelos con la finalidad de poder compararlos y ver cuál devuelve resultados más precisos.
El fraude afecta en muchos sectores y sigue creciendo diariamente, por lo que es una gran preocupación. Los modelos de ML son una de las soluciones que mejores resultados están reportando en la detección del fraude, por lo que son muy útiles y se pueden llegar a aplicar en entidades bancarias, proveedoras de tarjetas bancarias, páginas de e-commerce, entre otras.
Los datos se han obtenido de la plataforma Kaggle (https://www.kaggle.com/competitions/ieee-fraud-detection/), que pone a disposición de los usuarios un gran repositorio con conjuntos de datos de diversos temas. Concretamente, los datos seleccionados fueron publicados para un concurso organizado por el IEEE en colaboración con Vesta, una de las compañias pioneras en la proporción de soluciones para la prevención del fraude.
Son dos conjuntos de datos: Transaction y Identity. El primero contiene información sobre las transacciones y el segundo sobre los usuarios, tienen una columna común con la que se pueden relacionar. Cabe destacar que no hay una explicación concreta de todas las variables, ya que muchas de ellas están anonimizadas para preservar la privacidad de los usuarios, por lo que no se puede conocer qué información aportan exactamente. En el diccionario de datos se amplia todo lo que se sabe acerca de las distintas variables.
La variable objetivo es isfraud y hace referencia a si una transacción es fraudulenta o no, está codificada con 1 si es fraude y 0 si no lo es.
Este es el primer notebook del proyecto, en el que se va a llevar a cabo un análisis exploratorio de los datos para tener un conocimiento más amplio de los mismos y realizar algunas transformaciones iniciales si fuese necesario. El esquema que se va a seguir es el siguiente:
# Libraries
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import chi2_contingency
import seaborn as sns
import warnings
import datetime
warnings.filterwarnings('ignore')
# Functions
def cramers_V(var1,var2) :
crosstab = np.array(pd.crosstab(var1,var2, rownames=None, colnames=None))
stat = chi2_contingency(crosstab)[0]
obs = np.sum(crosstab)
mini = min(crosstab.shape)-1
return (stat/(obs*mini))
def get_deviation_of_mean_perc(pd_loan, list_var_continuous, target, multiplier):
pd_final = pd.DataFrame()
for i in list_var_continuous:
series_mean = pd_loan[i].mean()
series_std = pd_loan[i].std()
std_amp = multiplier * series_std
left = series_mean - std_amp
right = series_mean + std_amp
size_s = pd_loan[i].size
perc_goods = pd_loan[i][(pd_loan[i] >= left) & (pd_loan[i] <= right)].size/size_s
perc_excess = pd_loan[i][(pd_loan[i] < left) | (pd_loan[i] > right)].size/size_s
if perc_excess>0:
pd_concat_percent = pd.DataFrame(pd_loan[target][(pd_loan[i] < left) | (pd_loan[i] > right)]\
.value_counts(normalize=True).reset_index()).T
pd_concat_percent.columns = [pd_concat_percent.iloc[0,0],
pd_concat_percent.iloc[0,1]]
pd_concat_percent = pd_concat_percent.drop('index',axis=0)
pd_concat_percent['variable'] = i
pd_concat_percent['sum_outlier_values'] = pd_loan[i][(pd_loan[i] < left) | (pd_loan[i] > right)].size
pd_concat_percent['porcentaje_sum_outlier_values'] = perc_excess
pd_final = pd.concat([pd_final, pd_concat_percent], axis=0).reset_index(
drop=True).sort_values(by=['porcentaje_sum_outlier_values'], ascending=False)
if pd_final.empty:
print('No existen variables con valores outliers')
return pd_final
def reduce_group(grps,c='v'):
use = []
for g in grps:
mx = 0; vx = g[0]
for gg in g:
n = fraud_df[c+str(gg)].nunique()
if n>mx:
mx = n
vx = gg
use.append(vx)
print('Las columnas a mantener son:',use)
# I parameterize the names of the files to read.
train_identity = "../data/raw/train_identity.csv"
test_identity = "../data/raw/test_identity.csv"
train_transaction = "../data/raw/train_transaction.csv"
test_transaction = "../data/raw/test_transaction.csv"
# I'm only going to read the train files, because test files don't have isFraud column.
identity_df = pd.read_csv(train_identity)
transaction_df = pd.read_csv(train_transaction)
# I join identity and transaction df by the column they have in common (TransactionID). I specify 'outer' so that all
# transaction rows are kept because identity is a smaller data set and does not contain all id's.
fraud_df = pd.merge(transaction_df, identity_df, on='TransactionID', how='outer')
# I convert the names of all the columns to lower case, to facilitate their treatment.
fraud_df = fraud_df.rename(columns=str.lower)
fraud_df.head()
| transactionid | isfraud | transactiondt | transactionamt | productcd | card1 | card2 | card3 | card4 | card5 | ... | id_31 | id_32 | id_33 | id_34 | id_35 | id_36 | id_37 | id_38 | devicetype | deviceinfo | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2987000 | 0 | 86400 | 68.5 | W | 13926 | NaN | 150.0 | discover | 142.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 1 | 2987001 | 0 | 86401 | 29.0 | W | 2755 | 404.0 | 150.0 | mastercard | 102.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 2 | 2987002 | 0 | 86469 | 59.0 | W | 4663 | 490.0 | 150.0 | visa | 166.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 3 | 2987003 | 0 | 86499 | 50.0 | W | 18132 | 567.0 | 150.0 | mastercard | 117.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 4 | 2987004 | 0 | 86506 | 50.0 | H | 4497 | 514.0 | 150.0 | mastercard | 102.0 | ... | samsung browser 6.2 | 32.0 | 2220x1080 | match_status:2 | T | F | T | T | mobile | SAMSUNG SM-G892A Build/NRD90M |
5 rows × 434 columns
# I check the shape of the datasets, to make sure that they have been correct.
print(identity_df.shape)
print(transaction_df.shape)
print(fraud_df.shape)
(144233, 41) (590540, 394) (590540, 434)
El dataset resultante de la unión tiene 434 variables y 590540 filas
# I check for duplicate values.
print(fraud_df.shape, fraud_df.drop_duplicates().shape)
(590540, 434) (590540, 434)
A la izquierda está la dimensión del df y a la derecha si se eliminasen los valores duplicados. Se puede ver que no hay duplicados, puesto que la dimensión es la misma.
# I remove the transactionid column because it is the identifier of each transaction and it will not contribute anything
#to the models. Its main utility was to relate both datasets.
fraud_df = fraud_df.drop('transactionid', axis=1)
# I check the types of the variables.
fraud_df.dtypes.to_dict()
{'isfraud': dtype('int64'),
'transactiondt': dtype('int64'),
'transactionamt': dtype('float64'),
'productcd': dtype('O'),
'card1': dtype('int64'),
'card2': dtype('float64'),
'card3': dtype('float64'),
'card4': dtype('O'),
'card5': dtype('float64'),
'card6': dtype('O'),
'addr1': dtype('float64'),
'addr2': dtype('float64'),
'dist1': dtype('float64'),
'dist2': dtype('float64'),
'p_emaildomain': dtype('O'),
'r_emaildomain': dtype('O'),
'c1': dtype('float64'),
'c2': dtype('float64'),
'c3': dtype('float64'),
'c4': dtype('float64'),
'c5': dtype('float64'),
'c6': dtype('float64'),
'c7': dtype('float64'),
'c8': dtype('float64'),
'c9': dtype('float64'),
'c10': dtype('float64'),
'c11': dtype('float64'),
'c12': dtype('float64'),
'c13': dtype('float64'),
'c14': dtype('float64'),
'd1': dtype('float64'),
'd2': dtype('float64'),
'd3': dtype('float64'),
'd4': dtype('float64'),
'd5': dtype('float64'),
'd6': dtype('float64'),
'd7': dtype('float64'),
'd8': dtype('float64'),
'd9': dtype('float64'),
'd10': dtype('float64'),
'd11': dtype('float64'),
'd12': dtype('float64'),
'd13': dtype('float64'),
'd14': dtype('float64'),
'd15': dtype('float64'),
'm1': dtype('O'),
'm2': dtype('O'),
'm3': dtype('O'),
'm4': dtype('O'),
'm5': dtype('O'),
'm6': dtype('O'),
'm7': dtype('O'),
'm8': dtype('O'),
'm9': dtype('O'),
'v1': dtype('float64'),
'v2': dtype('float64'),
'v3': dtype('float64'),
'v4': dtype('float64'),
'v5': dtype('float64'),
'v6': dtype('float64'),
'v7': dtype('float64'),
'v8': dtype('float64'),
'v9': dtype('float64'),
'v10': dtype('float64'),
'v11': dtype('float64'),
'v12': dtype('float64'),
'v13': dtype('float64'),
'v14': dtype('float64'),
'v15': dtype('float64'),
'v16': dtype('float64'),
'v17': dtype('float64'),
'v18': dtype('float64'),
'v19': dtype('float64'),
'v20': dtype('float64'),
'v21': dtype('float64'),
'v22': dtype('float64'),
'v23': dtype('float64'),
'v24': dtype('float64'),
'v25': dtype('float64'),
'v26': dtype('float64'),
'v27': dtype('float64'),
'v28': dtype('float64'),
'v29': dtype('float64'),
'v30': dtype('float64'),
'v31': dtype('float64'),
'v32': dtype('float64'),
'v33': dtype('float64'),
'v34': dtype('float64'),
'v35': dtype('float64'),
'v36': dtype('float64'),
'v37': dtype('float64'),
'v38': dtype('float64'),
'v39': dtype('float64'),
'v40': dtype('float64'),
'v41': dtype('float64'),
'v42': dtype('float64'),
'v43': dtype('float64'),
'v44': dtype('float64'),
'v45': dtype('float64'),
'v46': dtype('float64'),
'v47': dtype('float64'),
'v48': dtype('float64'),
'v49': dtype('float64'),
'v50': dtype('float64'),
'v51': dtype('float64'),
'v52': dtype('float64'),
'v53': dtype('float64'),
'v54': dtype('float64'),
'v55': dtype('float64'),
'v56': dtype('float64'),
'v57': dtype('float64'),
'v58': dtype('float64'),
'v59': dtype('float64'),
'v60': dtype('float64'),
'v61': dtype('float64'),
'v62': dtype('float64'),
'v63': dtype('float64'),
'v64': dtype('float64'),
'v65': dtype('float64'),
'v66': dtype('float64'),
'v67': dtype('float64'),
'v68': dtype('float64'),
'v69': dtype('float64'),
'v70': dtype('float64'),
'v71': dtype('float64'),
'v72': dtype('float64'),
'v73': dtype('float64'),
'v74': dtype('float64'),
'v75': dtype('float64'),
'v76': dtype('float64'),
'v77': dtype('float64'),
'v78': dtype('float64'),
'v79': dtype('float64'),
'v80': dtype('float64'),
'v81': dtype('float64'),
'v82': dtype('float64'),
'v83': dtype('float64'),
'v84': dtype('float64'),
'v85': dtype('float64'),
'v86': dtype('float64'),
'v87': dtype('float64'),
'v88': dtype('float64'),
'v89': dtype('float64'),
'v90': dtype('float64'),
'v91': dtype('float64'),
'v92': dtype('float64'),
'v93': dtype('float64'),
'v94': dtype('float64'),
'v95': dtype('float64'),
'v96': dtype('float64'),
'v97': dtype('float64'),
'v98': dtype('float64'),
'v99': dtype('float64'),
'v100': dtype('float64'),
'v101': dtype('float64'),
'v102': dtype('float64'),
'v103': dtype('float64'),
'v104': dtype('float64'),
'v105': dtype('float64'),
'v106': dtype('float64'),
'v107': dtype('float64'),
'v108': dtype('float64'),
'v109': dtype('float64'),
'v110': dtype('float64'),
'v111': dtype('float64'),
'v112': dtype('float64'),
'v113': dtype('float64'),
'v114': dtype('float64'),
'v115': dtype('float64'),
'v116': dtype('float64'),
'v117': dtype('float64'),
'v118': dtype('float64'),
'v119': dtype('float64'),
'v120': dtype('float64'),
'v121': dtype('float64'),
'v122': dtype('float64'),
'v123': dtype('float64'),
'v124': dtype('float64'),
'v125': dtype('float64'),
'v126': dtype('float64'),
'v127': dtype('float64'),
'v128': dtype('float64'),
'v129': dtype('float64'),
'v130': dtype('float64'),
'v131': dtype('float64'),
'v132': dtype('float64'),
'v133': dtype('float64'),
'v134': dtype('float64'),
'v135': dtype('float64'),
'v136': dtype('float64'),
'v137': dtype('float64'),
'v138': dtype('float64'),
'v139': dtype('float64'),
'v140': dtype('float64'),
'v141': dtype('float64'),
'v142': dtype('float64'),
'v143': dtype('float64'),
'v144': dtype('float64'),
'v145': dtype('float64'),
'v146': dtype('float64'),
'v147': dtype('float64'),
'v148': dtype('float64'),
'v149': dtype('float64'),
'v150': dtype('float64'),
'v151': dtype('float64'),
'v152': dtype('float64'),
'v153': dtype('float64'),
'v154': dtype('float64'),
'v155': dtype('float64'),
'v156': dtype('float64'),
'v157': dtype('float64'),
'v158': dtype('float64'),
'v159': dtype('float64'),
'v160': dtype('float64'),
'v161': dtype('float64'),
'v162': dtype('float64'),
'v163': dtype('float64'),
'v164': dtype('float64'),
'v165': dtype('float64'),
'v166': dtype('float64'),
'v167': dtype('float64'),
'v168': dtype('float64'),
'v169': dtype('float64'),
'v170': dtype('float64'),
'v171': dtype('float64'),
'v172': dtype('float64'),
'v173': dtype('float64'),
'v174': dtype('float64'),
'v175': dtype('float64'),
'v176': dtype('float64'),
'v177': dtype('float64'),
'v178': dtype('float64'),
'v179': dtype('float64'),
'v180': dtype('float64'),
'v181': dtype('float64'),
'v182': dtype('float64'),
'v183': dtype('float64'),
'v184': dtype('float64'),
'v185': dtype('float64'),
'v186': dtype('float64'),
'v187': dtype('float64'),
'v188': dtype('float64'),
'v189': dtype('float64'),
'v190': dtype('float64'),
'v191': dtype('float64'),
'v192': dtype('float64'),
'v193': dtype('float64'),
'v194': dtype('float64'),
'v195': dtype('float64'),
'v196': dtype('float64'),
'v197': dtype('float64'),
'v198': dtype('float64'),
'v199': dtype('float64'),
'v200': dtype('float64'),
'v201': dtype('float64'),
'v202': dtype('float64'),
'v203': dtype('float64'),
'v204': dtype('float64'),
'v205': dtype('float64'),
'v206': dtype('float64'),
'v207': dtype('float64'),
'v208': dtype('float64'),
'v209': dtype('float64'),
'v210': dtype('float64'),
'v211': dtype('float64'),
'v212': dtype('float64'),
'v213': dtype('float64'),
'v214': dtype('float64'),
'v215': dtype('float64'),
'v216': dtype('float64'),
'v217': dtype('float64'),
'v218': dtype('float64'),
'v219': dtype('float64'),
'v220': dtype('float64'),
'v221': dtype('float64'),
'v222': dtype('float64'),
'v223': dtype('float64'),
'v224': dtype('float64'),
'v225': dtype('float64'),
'v226': dtype('float64'),
'v227': dtype('float64'),
'v228': dtype('float64'),
'v229': dtype('float64'),
'v230': dtype('float64'),
'v231': dtype('float64'),
'v232': dtype('float64'),
'v233': dtype('float64'),
'v234': dtype('float64'),
'v235': dtype('float64'),
'v236': dtype('float64'),
'v237': dtype('float64'),
'v238': dtype('float64'),
'v239': dtype('float64'),
'v240': dtype('float64'),
'v241': dtype('float64'),
'v242': dtype('float64'),
'v243': dtype('float64'),
'v244': dtype('float64'),
'v245': dtype('float64'),
'v246': dtype('float64'),
'v247': dtype('float64'),
'v248': dtype('float64'),
'v249': dtype('float64'),
'v250': dtype('float64'),
'v251': dtype('float64'),
'v252': dtype('float64'),
'v253': dtype('float64'),
'v254': dtype('float64'),
'v255': dtype('float64'),
'v256': dtype('float64'),
'v257': dtype('float64'),
'v258': dtype('float64'),
'v259': dtype('float64'),
'v260': dtype('float64'),
'v261': dtype('float64'),
'v262': dtype('float64'),
'v263': dtype('float64'),
'v264': dtype('float64'),
'v265': dtype('float64'),
'v266': dtype('float64'),
'v267': dtype('float64'),
'v268': dtype('float64'),
'v269': dtype('float64'),
'v270': dtype('float64'),
'v271': dtype('float64'),
'v272': dtype('float64'),
'v273': dtype('float64'),
'v274': dtype('float64'),
'v275': dtype('float64'),
'v276': dtype('float64'),
'v277': dtype('float64'),
'v278': dtype('float64'),
'v279': dtype('float64'),
'v280': dtype('float64'),
'v281': dtype('float64'),
'v282': dtype('float64'),
'v283': dtype('float64'),
'v284': dtype('float64'),
'v285': dtype('float64'),
'v286': dtype('float64'),
'v287': dtype('float64'),
'v288': dtype('float64'),
'v289': dtype('float64'),
'v290': dtype('float64'),
'v291': dtype('float64'),
'v292': dtype('float64'),
'v293': dtype('float64'),
'v294': dtype('float64'),
'v295': dtype('float64'),
'v296': dtype('float64'),
'v297': dtype('float64'),
'v298': dtype('float64'),
'v299': dtype('float64'),
'v300': dtype('float64'),
'v301': dtype('float64'),
'v302': dtype('float64'),
'v303': dtype('float64'),
'v304': dtype('float64'),
'v305': dtype('float64'),
'v306': dtype('float64'),
'v307': dtype('float64'),
'v308': dtype('float64'),
'v309': dtype('float64'),
'v310': dtype('float64'),
'v311': dtype('float64'),
'v312': dtype('float64'),
'v313': dtype('float64'),
'v314': dtype('float64'),
'v315': dtype('float64'),
'v316': dtype('float64'),
'v317': dtype('float64'),
'v318': dtype('float64'),
'v319': dtype('float64'),
'v320': dtype('float64'),
'v321': dtype('float64'),
'v322': dtype('float64'),
'v323': dtype('float64'),
'v324': dtype('float64'),
'v325': dtype('float64'),
'v326': dtype('float64'),
'v327': dtype('float64'),
'v328': dtype('float64'),
'v329': dtype('float64'),
'v330': dtype('float64'),
'v331': dtype('float64'),
'v332': dtype('float64'),
'v333': dtype('float64'),
'v334': dtype('float64'),
'v335': dtype('float64'),
'v336': dtype('float64'),
'v337': dtype('float64'),
'v338': dtype('float64'),
'v339': dtype('float64'),
'id_01': dtype('float64'),
'id_02': dtype('float64'),
'id_03': dtype('float64'),
'id_04': dtype('float64'),
'id_05': dtype('float64'),
'id_06': dtype('float64'),
'id_07': dtype('float64'),
'id_08': dtype('float64'),
'id_09': dtype('float64'),
'id_10': dtype('float64'),
'id_11': dtype('float64'),
'id_12': dtype('O'),
'id_13': dtype('float64'),
'id_14': dtype('float64'),
'id_15': dtype('O'),
'id_16': dtype('O'),
'id_17': dtype('float64'),
'id_18': dtype('float64'),
'id_19': dtype('float64'),
'id_20': dtype('float64'),
'id_21': dtype('float64'),
'id_22': dtype('float64'),
'id_23': dtype('O'),
'id_24': dtype('float64'),
'id_25': dtype('float64'),
'id_26': dtype('float64'),
'id_27': dtype('O'),
'id_28': dtype('O'),
'id_29': dtype('O'),
'id_30': dtype('O'),
'id_31': dtype('O'),
'id_32': dtype('float64'),
'id_33': dtype('O'),
'id_34': dtype('O'),
'id_35': dtype('O'),
'id_36': dtype('O'),
'id_37': dtype('O'),
'id_38': dtype('O'),
'devicetype': dtype('O'),
'deviceinfo': dtype('O')}
Los tipos con los que se han cargado las variables son esos. En la página web de la que se han descargado se indicaban los tipos correctos, y las variables categóricas son las siguientes:
El resto de las variables son tipo float o integer. Más adelante los analizaremos y cambiaremos si es necesario.
La variable target es isfraud que hace referencia a si una transacción es fraudulenta o no. Toma el valor 1 si es fraude y 0 en caso contrario.
# Firstly, I am going to position the target variable to the right of the dataset.
# I extract the column.
mov_card = fraud_df.pop('isfraud')
# I insert it as the last column.
fraud_df.insert(len(fraud_df.columns), "isfraud", mov_card)
# I add the values for each category (0 and 1) and calculate their percentage.
fraud_df_percent = fraud_df['isfraud']\
.value_counts(normalize = True)\
.mul(100).rename('percent').reset_index()
# I add the values for each category (0 and 1).
fraud_df_count = fraud_df['isfraud'].value_counts().reset_index()
# I join the previous results.
fraud_df_concat = pd.merge(fraud_df_percent,
fraud_df_count, on=['index'], how='inner')
fraud_df_concat
| index | percent | isfraud | |
|---|---|---|---|
| 0 | 0 | 96.500999 | 569877 |
| 1 | 1 | 3.499001 | 20663 |
# Display the result.
fig = plt.figure(figsize = (10,4))
ax = fig.add_axes([0,0,1,1])
lab = ['0', '1']
perc = fraud_df_concat['percent']
ax.bar(lab, perc, color = 'orange', alpha=0.8)
ax.set_title('Distribution of the target')
ax.set_ylabel('Percent')
plt.show()
Como conclusión a los cálculos y al gráfico, se puede afirmar que el dataset está excesivamente desbalanceado en favor de las transacciones no fraudulentas (0). El 96,50% de las transacciones que se analizan en este dataset no son fraude y solo el 3,50% lo son.
# I calculate the nulls by columns and the percentage
fraud_df_null_columns = fraud_df.isnull().sum().sort_values(ascending=False).to_frame('columns_null').reset_index()
fraud_df_null_columns['columns_percentage'] = fraud_df_null_columns['columns_null']/fraud_df.shape[0] * 100
fraud_df_null_columns
| index | columns_null | columns_percentage | |
|---|---|---|---|
| 0 | id_24 | 585793 | 99.196159 |
| 1 | id_25 | 585408 | 99.130965 |
| 2 | id_07 | 585385 | 99.127070 |
| 3 | id_08 | 585385 | 99.127070 |
| 4 | id_21 | 585381 | 99.126393 |
| ... | ... | ... | ... |
| 428 | c10 | 0 | 0.000000 |
| 429 | c11 | 0 | 0.000000 |
| 430 | c12 | 0 | 0.000000 |
| 431 | c13 | 0 | 0.000000 |
| 432 | isfraud | 0 | 0.000000 |
433 rows × 3 columns
Hay columnas con un elevado porcentaje de valores nulos, lo que podría afectar al modelo. El dataset tiene 434 columnas, por lo que se podría eliminar un número considerable de variables, puesto que disponemos de suficientes como para construir un modelo. Se van a eliminar todas aquellas que tengan más de un 50% de valores nulos (unas 300.000 filas), ya que es un número elevado como para imputar tantos datos, podría afectar a la variable. El resto de columnas con valores nulos se tratarán más adelante.
# I create a list with the columns that have more than 50% null values
cols_to_drop = fraud_df_null_columns.loc[fraud_df_null_columns['columns_percentage'] > 50, 'index'].tolist()
# I remove the selected columns
fraud_df = fraud_df.drop(cols_to_drop, axis=1)
# I calculate the nulls by rows, if there is any row with the variable target null and the percentage of nulls of each row
fraud_df_null_rows = fraud_df.isnull().sum(axis=1).sort_values(ascending=False)
fraud_df_null_rows = pd.DataFrame(fraud_df_null_rows, columns=['rows_null'])
fraud_df_null_rows['target'] = fraud_df['isfraud'].copy()
fraud_df_null_rows['rows_percentage']= fraud_df_null_rows['rows_null']/fraud_df.shape[1] * 100
fraud_df_null_rows
| rows_null | target | rows_percentage | |
|---|---|---|---|
| 456104 | 140 | 0 | 63.926941 |
| 456122 | 139 | 0 | 63.470320 |
| 474061 | 139 | 0 | 63.470320 |
| 474362 | 139 | 0 | 63.470320 |
| 474068 | 139 | 0 | 63.470320 |
| ... | ... | ... | ... |
| 243278 | 0 | 0 | 0.000000 |
| 243280 | 0 | 0 | 0.000000 |
| 503144 | 0 | 0 | 0.000000 |
| 243282 | 0 | 0 | 0.000000 |
| 295270 | 0 | 0 | 0.000000 |
590540 rows × 3 columns
El valor más alto de nulos que encontramos en las filas ronda el 63% lo que equivale a unas 139 - 140 variables faltantes en una transaccion. Se van a eliminar aquellas observaciones que tienen más de un 50% de valores nulos, dado que no son demasiadas y apenas afecta al dataset.
nulls_50 = list(fraud_df_null_rows.index[fraud_df_null_rows['rows_percentage'] > 50])
fraud_df = fraud_df.drop(index = nulls_50)
fraud_df.shape
(590472, 219)
Tras eliminar las columnas y filas con más de un 50% de nulos, el dataset resultante tiene 219 variables y 590472 filas.
Las variables categóricas según la información que hay sobre el dataset son las siguientes:
Por otra parte, han indicado que algunas de las variables nombradas como v[...] explican relación entre entidades, por lo que podrían ser binarias y se deberían tratar como categóricas. Para ello, se van a buscar aquellas que solo tienen como valores únicos el 0 y el 1.
# I create a loop to search for the variables v[...] that are binary, those that have unique values 0 and 1.
# I add them to var_binary list.
var_binary = []
for col in fraud_df.columns:
if fraud_df[col].nunique() == 2 and set([0, 1]) == set(fraud_df[col].dropna().unique()):
var_binary.append(col)
# I create a list with the other categorical variables.
var_category = [
'productcd', 'card1', 'card2', 'card3', 'card4', 'card5', 'card6', 'addr1', 'addr2', 'p_emaildomain',
'm1', 'm2', 'm3', 'm4', 'm6'
]
# I join two list
var_category.extend(var_binary)
# I change the type to object to make sure they have the correct type.
fraud_df[var_category] = fraud_df[var_category].astype("object")
# I check the number of unique values of each variable.
for i in fraud_df[var_category]:
print(i, fraud_df[i].nunique())
productcd 5 card1 13548 card2 500 card3 114 card4 4 card5 119 card6 4 addr1 332 addr2 74 p_emaildomain 59 m1 2 m2 2 m3 2 m4 3 m6 2 v1 2 v14 2 v41 2 v65 2 v88 2 v107 2 isfraud 2
# I check the unique values of each variable.
for i in fraud_df[var_category]:
print(i, fraud_df[i].unique())
productcd ['W' 'H' 'C' 'S' 'R'] card1 [13926 2755 4663 ... 17972 13166 8767] card2 [nan 404.0 490.0 567.0 514.0 555.0 360.0 100.0 111.0 352.0 375.0 418.0 303.0 314.0 543.0 583.0 148.0 321.0 269.0 361.0 272.0 399.0 569.0 453.0 417.0 512.0 545.0 266.0 114.0 481.0 452.0 547.0 383.0 170.0 343.0 556.0 285.0 562.0 302.0 264.0 558.0 500.0 396.0 103.0 206.0 143.0 243.0 476.0 199.0 174.0 423.0 446.0 492.0 523.0 440.0 528.0 161.0 535.0 354.0 117.0 455.0 325.0 158.0 268.0 122.0 479.0 147.0 215.0 480.0 265.0 388.0 408.0 309.0 415.0 414.0 437.0 104.0 225.0 101.0 134.0 586.0 191.0 491.0 369.0 322.0 494.0 532.0 313.0 474.0 324.0 475.0 298.0 429.0 432.0 553.0 566.0 599.0 296.0 251.0 310.0 242.0 204.0 250.0 270.0 346.0 316.0 194.0 587.0 390.0 135.0 536.0 254.0 226.0 327.0 420.0 260.0 413.0 428.0 561.0 387.0 411.0 392.0 203.0 297.0 136.0 276.0 142.0 527.0 210.0 184.0 459.0 118.0 585.0 106.0 588.0 449.0 176.0 177.0 246.0 439.0 503.0 445.0 172.0 468.0 239.0 496.0 364.0 533.0 370.0 578.0 150.0 458.0 365.0 359.0 509.0 202.0 584.0 258.0 442.0 530.0 489.0 529.0 504.0 356.0 123.0 205.0 130.0 382.0 155.0 105.0 422.0 345.0 257.0 171.0 559.0 271.0 554.0 127.0 236.0 548.0 454.0 373.0 275.0 286.0 308.0 517.0 294.0 145.0 565.0 245.0 513.0 520.0 133.0 162.0 163.0 216.0 550.0 355.0 180.0 393.0 391.0 462.0 435.0 460.0 444.0 333.0 595.0 443.0 507.0 551.0 572.0 110.0 470.0 472.0 102.0 144.0 300.0 400.0 478.0 198.0 600.0 167.0 350.0 320.0 579.0 113.0 560.0 389.0 431.0 589.0 592.0 336.0 295.0 515.0 108.0 385.0 281.0 231.0 339.0 128.0 367.0 301.0 280.0 510.0 426.0 357.0 593.0 467.0 262.0 160.0 315.0 571.0 394.0 166.0 304.0 485.0 376.0 448.0 537.0 247.0 594.0 461.0 181.0 581.0 542.0 582.0 499.0 287.0 519.0 381.0 424.0 214.0 525.0 332.0 311.0 340.0 368.0 168.0 183.0 371.0 516.0 342.0 112.0 278.0 115.0 384.0 229.0 234.0 374.0 291.0 596.0 165.0 539.0 409.0 318.0 126.0 568.0 549.0 146.0 152.0 253.0 347.0 290.0 348.0 240.0 201.0 307.0 283.0 222.0 248.0 464.0 534.0 299.0 450.0 218.0 192.0 255.0 197.0 341.0 330.0 570.0 501.0 574.0 200.0 219.0 337.0 372.0 317.0 433.0 505.0 477.0 159.0 284.0 552.0 502.0 405.0 563.0 306.0 193.0 140.0 334.0 540.0 169.0 487.0 353.0 141.0 109.0 386.0 471.0 208.0 524.0 116.0 209.0 416.0 132.0 157.0 182.0 154.0 434.0 395.0 398.0 220.0 484.0 282.0 511.0 277.0 173.0 402.0 274.0 178.0 237.0 305.0 427.0 252.0 196.0 463.0 244.0 149.0 531.0 238.0 573.0 338.0 493.0 412.0 380.0 473.0 153.0 575.0 190.0 495.0 289.0 465.0 541.0 362.0 119.0 235.0 164.0 131.0 598.0 151.0 441.0 217.0 436.0 228.0 139.0 259.0 189.0 379.0 263.0 188.0 508.0 233.0 256.0 344.0 328.0 438.0 249.0 323.0 213.0 366.0 273.0 377.0 577.0 521.0 564.0 358.0 544.0 288.0 279.0 241.0 267.0 497.0 456.0 351.0 221.0 211.0 227.0 121.0 230.0 498.0 466.0 425.0 506.0 335.0 597.0 469.0 486.0 401.0 488.0 421.0 406.0 224.0 124.0 195.0 129.0 363.0 292.0 175.0 590.0 430.0 319.0 212.0 331.0 329.0 483.0 349.0 457.0 518.0 538.0 261.0 410.0 232.0 403.0 419.0 397.0 591.0 137.0 447.0 156.0 378.0 138.0 407.0 207.0 185.0 125.0 451.0 293.0 186.0 120.0 482.0 326.0 179.0 576.0 580.0 546.0 522.0 526.0 187.0 223.0 557.0 312.0] card3 [150.0 117.0 185.0 143.0 144.0 163.0 146.0 191.0 162.0 119.0 147.0 100.0 135.0 137.0 138.0 102.0 213.0 106.0 214.0 148.0 210.0 203.0 194.0 141.0 225.0 188.0 193.0 208.0 133.0 223.0 134.0 153.0 131.0 200.0 149.0 116.0 222.0 220.0 197.0 129.0 206.0 195.0 204.0 127.0 142.0 nan 111.0 159.0 118.0 229.0 217.0 212.0 227.0 166.0 189.0 120.0 171.0 190.0 105.0 130.0 170.0 180.0 177.0 132.0 109.0 198.0 121.0 183.0 215.0 156.0 207.0 186.0 167.0 152.0 199.0 219.0 202.0 126.0 182.0 123.0 107.0 221.0 124.0 231.0 157.0 136.0 128.0 168.0 101.0 108.0 125.0 211.0 164.0 160.0 161.0 179.0 155.0 169.0 205.0 209.0 226.0 174.0 176.0 181.0 224.0 122.0 201.0 175.0 139.0 172.0 114.0 228.0 184.0 151.0 173.0] card4 ['discover' 'mastercard' 'visa' 'american express' nan] card5 [142.0 102.0 166.0 117.0 226.0 224.0 134.0 219.0 137.0 195.0 138.0 100.0 147.0 162.0 202.0 118.0 150.0 183.0 171.0 236.0 197.0 133.0 223.0 149.0 228.0 229.0 nan 198.0 182.0 126.0 185.0 190.0 131.0 144.0 141.0 215.0 203.0 237.0 132.0 143.0 146.0 140.0 129.0 199.0 194.0 213.0 111.0 169.0 177.0 173.0 156.0 119.0 135.0 107.0 232.0 188.0 159.0 127.0 148.0 139.0 104.0 210.0 180.0 207.0 204.0 212.0 106.0 152.0 222.0 187.0 200.0 214.0 189.0 181.0 206.0 225.0 157.0 121.0 217.0 184.0 167.0 113.0 136.0 120.0 201.0 211.0 122.0 164.0 123.0 145.0 172.0 114.0 231.0 105.0 221.0 234.0 130.0 109.0 196.0 101.0 158.0 230.0 128.0 191.0 165.0 115.0 233.0 216.0 116.0 178.0 125.0 163.0 205.0 112.0 168.0 209.0 235.0 151.0 175.0 160.0] card6 ['credit' 'debit' 'debit or credit' 'charge card' nan] addr1 [315.0 325.0 330.0 476.0 420.0 272.0 126.0 337.0 204.0 nan 226.0 170.0 184.0 264.0 299.0 441.0 472.0 251.0 469.0 191.0 485.0 122.0 220.0 205.0 387.0 181.0 231.0 436.0 327.0 343.0 123.0 502.0 269.0 158.0 433.0 143.0 225.0 492.0 177.0 512.0 310.0 308.0 418.0 494.0 253.0 428.0 203.0 110.0 444.0 498.0 194.0 536.0 384.0 324.0 295.0 448.0 157.0 171.0 224.0 491.0 274.0 432.0 459.0 106.0 296.0 254.0 452.0 347.0 335.0 305.0 161.0 221.0 154.0 130.0 465.0 451.0 331.0 511.0 486.0 312.0 242.0 131.0 508.0 332.0 283.0 216.0 431.0 391.0 333.0 496.0 304.0 167.0 261.0 399.0 164.0 142.0 393.0 258.0 375.0 483.0 404.0 482.0 148.0 218.0 127.0 403.0 374.0 477.0 478.0 241.0 504.0 453.0 535.0 100.0 239.0 152.0 500.0 356.0 198.0 162.0 244.0 520.0 531.0 401.0 427.0 352.0 540.0 456.0 371.0 409.0 529.0 505.0 503.0 346.0 359.0 499.0 298.0 190.0 454.0 172.0 145.0 493.0 119.0 521.0 270.0 395.0 366.0 348.0 313.0 338.0 139.0 443.0 183.0 430.0 102.0 470.0 133.0 233.0 468.0 185.0 523.0 411.0 501.0 425.0 426.0 365.0 129.0 141.0 137.0 249.0 402.0 386.0 290.0 111.0 132.0 278.0 506.0 128.0 213.0 514.0 314.0 445.0 252.0 328.0 210.0 144.0 193.0 382.0 306.0 385.0 235.0 339.0 211.0 248.0 163.0 199.0 180.0 217.0 146.0 408.0 329.0 372.0 518.0 326.0 530.0 345.0 488.0 260.0 300.0 527.0 323.0 187.0 379.0 178.0 153.0 466.0 189.0 302.0 464.0 307.0 434.0 406.0 467.0 117.0 474.0 234.0 341.0 400.0 351.0 292.0 416.0 528.0 112.0 219.0 462.0 200.0 360.0 516.0 353.0 101.0 513.0 201.0 120.0 526.0 257.0 471.0 262.0 397.0 450.0 156.0 280.0 243.0 439.0 489.0 389.0 214.0 410.0 159.0 168.0 297.0 236.0 519.0 349.0 134.0 275.0 479.0 515.0 277.0 265.0 522.0 294.0 463.0 368.0 182.0 276.0 376.0 301.0 104.0 435.0 247.0 232.0 202.0 309.0 373.0 259.0 381.0 340.0 369.0 255.0 105.0 250.0 160.0 282.0 208.0 125.0 429.0 227.0 509.0 174.0 390.0 321.0 166.0 303.0 322.0 284.0 316.0 458.0 196.0 215.0 155.0 279.0 446.0 396.0 358.0 195.0 361.0 151.0 417.0 223.0 124.0 113.0 377.0 238.0 237.0 206.0 517.0 318.0 481.0 286.0 507.0 457.0 268.0 245.0] addr2 [87.0 nan 96.0 35.0 60.0 98.0 43.0 65.0 32.0 13.0 31.0 101.0 24.0 16.0 15.0 19.0 71.0 59.0 102.0 44.0 26.0 69.0 47.0 78.0 88.0 66.0 72.0 22.0 57.0 25.0 17.0 30.0 29.0 21.0 14.0 49.0 83.0 75.0 34.0 86.0 48.0 68.0 23.0 70.0 62.0 54.0 50.0 52.0 39.0 76.0 10.0 73.0 97.0 63.0 27.0 28.0 38.0 74.0 77.0 92.0 79.0 84.0 82.0 40.0 36.0 46.0 18.0 20.0 89.0 61.0 94.0 100.0 55.0 51.0 93.0] p_emaildomain [nan 'gmail.com' 'outlook.com' 'yahoo.com' 'mail.com' 'anonymous.com' 'hotmail.com' 'verizon.net' 'aol.com' 'me.com' 'comcast.net' 'optonline.net' 'cox.net' 'charter.net' 'rocketmail.com' 'prodigy.net.mx' 'embarqmail.com' 'icloud.com' 'live.com.mx' 'gmail' 'live.com' 'att.net' 'juno.com' 'ymail.com' 'sbcglobal.net' 'bellsouth.net' 'msn.com' 'q.com' 'yahoo.com.mx' 'centurylink.net' 'servicios-ta.com' 'earthlink.net' 'hotmail.es' 'cfl.rr.com' 'roadrunner.com' 'netzero.net' 'gmx.de' 'suddenlink.net' 'frontiernet.net' 'windstream.net' 'frontier.com' 'outlook.es' 'mac.com' 'netzero.com' 'aim.com' 'web.de' 'twc.com' 'cableone.net' 'yahoo.fr' 'yahoo.de' 'yahoo.es' 'sc.rr.com' 'ptd.net' 'live.fr' 'yahoo.co.uk' 'hotmail.fr' 'hotmail.de' 'hotmail.co.uk' 'protonmail.com' 'yahoo.co.jp'] m1 ['T' nan 'F'] m2 ['T' nan 'F'] m3 ['T' nan 'F'] m4 ['M2' 'M0' nan 'M1'] m6 ['T' 'F' nan] v1 [1.0 nan 0.0] v14 [1.0 nan 0.0] v41 [nan 1.0 0.0] v65 [1.0 nan 0.0] v88 [1.0 nan 0.0] v107 [1.0 0.0 nan] isfraud [0 1]
Voy a representar algunas de las variables que tienen menos valores únicos para ver qué categorías están más afectadas por el fraude y conocer el comportamiento de los estafadores.
# We create a dataframe by grouping the data based on delivery and adding the stars.
fraud_productcd = fraud_df[['productcd', 'isfraud']].groupby('productcd')['isfraud'].sum().to_frame().reset_index()
# We visualize the results
fig = plt.figure(figsize = (10,4))
ax = fig.add_axes([0,0,1,1])
lab = list(fraud_productcd['productcd'])
fraud = fraud_productcd['isfraud']
ax.bar(lab, fraud, color = 'orange', alpha=0.8)
ax.set_title('Fraude según producto')
ax.set_ylabel('Valor')
plt.show()
Esta variable hace referencia al código de producto, cada uno de ellos representa el tipo de producto que ha sido objeto en la transacción. No hay información disponible sobre qué producto concreto representa cada valor, pero podemos ver que los C y W son en los que más fraude se producen y los S los que menos.
# We create a dataframe by grouping the data based on delivery and adding the stars.
fraud_productcd = fraud_df[['card4', 'isfraud']].groupby('card4')['isfraud'].sum().to_frame().reset_index()
# We visualize the results
fig = plt.figure(figsize = (4,4))
ax = fig.add_axes([0,0,1,1])
lab = list(fraud_productcd['card4'])
fraud = fraud_productcd['isfraud']
ax.bar(lab, fraud, color = 'orange', alpha=0.8)
ax.set_title('Fraude según proveedor de tarjeta')
ax.set_ylabel('Valor')
plt.show()
Esta variable indica el proveedor de la tarjeta que se ha usado en la transacción: american express, discover, mastercard o visa. Las tarjetas mastercard y visa son con las que más fraudes se realizan y las american express con las que menos.
# We create a dataframe by grouping the data based on delivery and adding the stars.
fraud_productcd = fraud_df[['card6', 'isfraud']].groupby('card6')['isfraud'].sum().to_frame().reset_index()
# We visualize the results
fig = plt.figure(figsize = (4,4))
ax = fig.add_axes([0,0,1,1])
lab = list(fraud_productcd['card6'])
fraud = fraud_productcd['isfraud']
ax.bar(lab, fraud, color = 'orange', alpha=0.8)
ax.set_title('Fraude según tipo de tarjeta')
ax.set_ylabel('Valor')
plt.show()
Esta variable hace referencia al tipo de tarjeta que se ha usado en la transacción. Hay cuatro categorías: crédito (credit), débito (debit), recarga (charge card) y, por último, crédito o débito (debit or credit) para transacciones que no está claro el tipo de tarjeta. Las de débito son las más usadas para el fraude y las de crédito también tienen un alto porcentaje. En las otras dos categorías no se han producido fraudes, en el caso de las tarjetas recargables tiene sentido puesto que su principal objetivo es evitarlo, ya que sus usuarios las recargan exclusivamente con la cantidad a gastar.
# We create a dataframe by grouping the data based on delivery and adding the stars.
fraud_productcd = fraud_df[['m1', 'isfraud']].groupby('m1')['isfraud'].sum().to_frame().reset_index()
# We visualize the results
fig = plt.figure(figsize = (10,4))
ax = fig.add_axes([0,0,1,1])
lab = list(fraud_productcd['m1'])
fraud = fraud_productcd['isfraud']
ax.bar(lab, fraud, color = 'orange', alpha=0.8)
ax.set_title('Fraude según m1')
ax.set_ylabel('Valor')
plt.show()
# We create a dataframe by grouping the data based on delivery and adding the stars.
fraud_productcd = fraud_df[['m2', 'isfraud']].groupby('m2')['isfraud'].sum().to_frame().reset_index()
# We visualize the results
fig = plt.figure(figsize = (10,4))
ax = fig.add_axes([0,0,1,1])
lab = list(fraud_productcd['m2'])
fraud = fraud_productcd['isfraud']
ax.bar(lab, fraud, color = 'orange', alpha=0.8)
ax.set_title('Fraude según m2')
ax.set_ylabel('Valor')
plt.show()
# We create a dataframe by grouping the data based on delivery and adding the stars.
fraud_productcd = fraud_df[['m3', 'isfraud']].groupby('m3')['isfraud'].sum().to_frame().reset_index()
# We visualize the results
fig = plt.figure(figsize = (10,4))
ax = fig.add_axes([0,0,1,1])
lab = list(fraud_productcd['m3'])
fraud = fraud_productcd['isfraud']
ax.bar(lab, fraud, color = 'orange', alpha=0.8)
ax.set_title('Fraude según m3')
ax.set_ylabel('Valor')
plt.show()
# We create a dataframe by grouping the data based on delivery and adding the stars.
fraud_productcd = fraud_df[['m6', 'isfraud']].groupby('m6')['isfraud'].sum().to_frame().reset_index()
# We visualize the results
fig = plt.figure(figsize = (10,4))
ax = fig.add_axes([0,0,1,1])
lab = list(fraud_productcd['m6'])
fraud = fraud_productcd['isfraud']
ax.bar(lab, fraud, color = 'orange', alpha=0.8)
ax.set_title('Fraude según m6')
ax.set_ylabel('Valor')
plt.show()
Estas variables se van a analizar conjuntamente dado que no se dispone de información concreta de cada una, solo se conoce que realizan una verificación de los datos, como ya se ha indicado. Toman el valor T (True) si coinciden y False (F) si no.
Por lo tanto, se puede comprobar que la mayoría de los datos personales suelen coincider, dado que m1, m2 y m3 alcanzan los valores más altos en True, mientras que m6 lo hace en False. Es probable que el dato no coincidente corresponda a la dirección porque los estafadores suelen obtener toda la información de los usuarios y envian los productos comprados a otra dirección para beneficiarse de ellos.
Como conclusión:
rows= []
for i1 in var_category:
col = []
for i2 in var_category:
cramers = cramers_V(fraud_df[i1], fraud_df[i2])
col.append(round(cramers,1))
rows.append(col)
cramers_results = np.array(rows)
v_c = pd.DataFrame(cramers_results, columns = var_category, index = var_category)
plt.figure(figsize=(20,15))
sns.heatmap(v_c, cmap="YlGnBu", annot=True, linewidth=.5)
plt.show()
Dada la alta correlación entre la variable card1 y card2, card3, card4 y card5, se va a eliminar card1 porque aunque no se tiene claro su significado, tiene un número muy elevado de valores para ser categórica y es posible que las otras variables contengan la información de esta y de ahí su alta correlación.
Esta misma situación se da con las variables binarias v41 - v88, por lo que se se va a eliminar una de ellas, puesto que la información podría ser redundante y afectar al modelo. Descartamos v41.
# I drop card1 and v41 column
fraud_df = fraud_df.drop(['card1', 'v41'], axis=1)
Como ya se ha comentado antes, la mayoría de las variables están anonimizadas, por lo que solo tenemos una idea general del siginificado de su significado:
A continuación se van a visualizar algunas de las variables que se consideran más interesantes y de las que se conoce su significado.
En primer lugar, se va a visualizar la evolución a lo largo del tiempo. Esto se puede obtener con la columna transactiondt que es la diferencia en segundos desde el día que comienzan los datos hasta el momento en el que se realiza la transacción.
En la información del dataset han publicado que los datos dan comienzo entre noviembre - diciembre de 2017. Se va a establecer como fecha de inicio el 01/12/2017.
# Define start date
START_DATE = '2017-12-01'
startdate = datetime.datetime.strptime(START_DATE, '%Y-%m-%d')
# Create a column with date and hour
fraud_df['transaction_date'] = fraud_df['transactiondt'].apply(lambda x: (startdate + datetime.timedelta(seconds = x)))
# Calculate the day of the transaction
fraud_df['transaction_day'] = (fraud_df['transaction_date'] - startdate).dt.days
# Calculate the hour of the transaction
fraud_df['transaction_hour'] = fraud_df['transaction_date'].dt.hour
fraud_df.groupby(['isfraud', 'transaction_day']).size().unstack(level=0).plot(kind='line', figsize = (10,6))
plt.title('Transacciones fraudulentas y no fraudulentas a lo largo del tiempo')
plt.xlabel('Día')
plt.ylabel('Cantidad de transacciones')
plt.legend(['No-Fraudulent', 'Fraudulent'])
plt.savefig('evolucion_transaccion.jpg')
plt.show()
En términos generales no hay un comportamiento exacto para el número de transacciones que se han realizado cada día. Si se aprecia un claro aumento a los 20 - 30 días, período que coincidiría aproximadamente con las festividades navideñas, cuando se realizan más compras.
También, hay una gran diferencia de magnitud entre las transacciones fraudulentas y las no fraudulentas, esto ya lo veíamos al explorar la variable target.
plt.figure(figsize=(15, 4))
fraud_df[fraud_df['isfraud'] == 1].groupby('transaction_day').size().plot(kind='line', color='orange', lw=1)
plt.title('Transacciones fraudulentas a lo largo del tiempo')
plt.xlabel('Día')
plt.ylabel('Cantidad de transacciones')
plt.show()
Si nos centramos exclusivamente en las fraudulentas, no se aprecía tanto ese incremento en el período navideño. El máximo se alcanza aproximadamente en los 60 días. Por otra parte, sí se aprecía una disminución en los últimos días.
fraud_df.groupby(['isfraud', 'transaction_hour']).size().unstack(level=0).plot(kind='line', figsize=(15, 4))
plt.title('Transacciones fraudulentas y no fraudulentas por horas')
plt.xlabel('Día')
plt.ylabel('Cantidad de transacciones')
plt.legend(['No-Fraudulent', 'Fraudulent'])
plt.show()
La mayor parte de las transacciones se realizan por la tarde, a partir de las 20:00 las no fraudulentas comienzan a disminuir hasta la mañana.
plt.figure(figsize=(10, 4))
fraud_df[fraud_df['isfraud'] == 1].groupby(fraud_df['transaction_hour']).size().plot(kind='line', color='orange', lw=1, title='Transacciones fraudulentas por hora del día')
plt.xlabel('Hora del día')
plt.ylabel('Cantidad de transacciones fraudulentas')
plt.xticks(np.arange(0, 24))
plt.savefig('fraude_hora.jpg')
plt.show()
Analizando las fraudulentas se aprecía un comportamiento muy similar pero, a partir de las 20 - 21 se aprecia un aumento, los estafadores aprovechan las horas en las que las victimas duermen para realizar los cargos.
# Representation of the frequency and density of the amount of the transaction
sns.set_style("white")
plt.figure(figsize=(10, 4))
sns.histplot(data=fraud_df[fraud_df['isfraud'] == 1], x='transactionamt', bins=50, kde=False, edgecolor='orange', color='orange', alpha=0.4, stat='density')
sns.kdeplot(data=fraud_df[fraud_df['isfraud'] == 1], x='transactionamt', color='orange', lw=2, linestyle='--')
plt.title('Histograma y densidad de la cantidad de las transacciones')
plt.xlabel('Cantidad')
plt.ylabel('Frecuencia / Densidad')
plt.savefig('fraude_densidad_valor.jpg')
plt.show()
La mayoría de las transacciones fraudulentas son de importes bajos, ya que principalmente se concentran en todo a los 100 - 200 dólares y a partir de los 1000 la frecuencia comienza a ser muy baja.
Esto se debe a que los estafadores suelen comenzar a hacer transacciones de cantidades pequeñas para verificar el funcionamiento de la tarjeta y posteriormente van incrementando. Además de que en ocasiones, es necesario que el propio cliente confirme la compra si es superior a cierta cantidad, lo que limita las cuantías que pueden robar. La detección temprana de estas actividades por parte de las víctimas también ayuda a una menor cantidad de transacciones fraudulentas con cuantías elevadas, como se puede ver en la visualización.
Este hecho resalta la importancia de un control constante de las cuentas, lo cual permitirá a las víctimas reaccionar rápido, proteger sus cuentas y prevenir pérdidas adicionales.
# I create a list with all the numeric variables.
var_num = [
'transactiondt', 'transactionamt', 'c1','c2','c3','c4','c5','c6','c7','c8','c9','c10', 'c11','c12',
'c13','c14','d1','d2','d3','d4','d10','d11','d15', 'v2','v3','v4','v5','v6','v7','v8','v9','v10','v11','v12','v13',
'v15', 'v16','v17','v18','v19','v20','v21','v22','v23','v24','v25','v26','v29', 'v30','v31','v32','v33','v34',
'v35','v36','v37','v38','v39','v40','v42','v43','v44','v45','v46','v47', 'v48','v49','v50','v51','v52','v53',
'v54','v55','v56','v57','v58','v59','v60','v61','v62','v63','v64','v66', 'v67', 'v69','v70', 'v71','v72','v73',
'v74','v75', 'v76','v77','v78','v79','v80','v81','v82','v83','v84','v85', 'v86','v87','v90', 'v91','v92','v93',
'v94','v98','v99','v100','v104','v105','v106','v108','v109', 'v110','v111','v112','v113','v114','v115','v116',
'v117','v118','v119','v120','v121','v122','v123','v124', 'v125','v126','v127','v128','v129','v130','v131','v132',
'v133','v134','v135','v136','v137', 'v279','v280','v281', 'v282','v283','v284','v285','v286','v287','v288','v289',
'v290','v291','v292','v293','v294','v295','v296','v297', 'v298','v299','v300', 'v301','v302','v303','v304','v306',
'v307','v308','v309','v310', 'v311','v312','v313','v314', 'v315','v316','v317','v318','v319','v320','v321'
]
# I use the function 'get_deviation_of_mean_perc' that I have imported to calculate the outliers.
get_deviation_of_mean_perc(fraud_df, var_num, target='isfraud', multiplier=1.5)
| 0.0 | 1.0 | variable | sum_outlier_values | porcentaje_sum_outlier_values | |
|---|---|---|---|---|---|
| 0 | 0.920688 | 0.079312 | v302 | 140294 | 0.237596 |
| 1 | 0.963370 | 0.036630 | v62 | 114906 | 0.194600 |
| 2 | 0.970584 | 0.029416 | v20 | 112522 | 0.190563 |
| 3 | 0.967895 | 0.032105 | v61 | 109237 | 0.184999 |
| 4 | 0.971485 | 0.028515 | v19 | 108786 | 0.184236 |
| ... | ... | ... | ... | ... | ... |
| 180 | 0.893849 | 0.106151 | v120 | 1008 | 0.001707 |
| 181 | 0.786437 | 0.213563 | v118 | 988 | 0.001673 |
| 182 | 0.733247 | 0.266753 | v119 | 776 | 0.001314 |
| 183 | 0.720770 | 0.279230 | v117 | 727 | 0.001231 |
| 184 | 0.814371 | 0.185629 | v291 | 334 | 0.000566 |
186 rows × 5 columns
A continuación, voy a realizar un análisis exploratorio de las variables que tienen más de un 15% de outliers para analizarlos correctamente y decidir como tratarlos.
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
sns.boxplot(x='v302', y='isfraud', data=fraud_df, orient='h', ax=axs[0])
axs[0].set_title('v302 by isfraud')
fraud_df.groupby(('v302'))['isfraud'].count().plot(kind="bar", ax=axs[1])
axs[1].set_title("v302 by isfraud")
plt.tight_layout()
plt.show()
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
sns.boxplot(x='v62', y='isfraud', data=fraud_df, orient='h', ax=axs[0])
axs[0].set_title('v62 by isfraud')
fraud_df.groupby(('v62'))['isfraud'].count().plot(kind="bar", ax=axs[1])
axs[1].set_title("v62 by isfraud")
plt.tight_layout()
plt.show()
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
sns.boxplot(x='v20', y='isfraud', data=fraud_df, orient='h', ax=axs[0])
axs[0].set_title('v20 by isfraud')
fraud_df.groupby(('v20'))['isfraud'].count().plot(kind="bar", ax=axs[1])
axs[1].set_title("v20 by isfraud")
plt.tight_layout()
plt.show()
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
sns.boxplot(x='v61', y='isfraud', data=fraud_df, orient='h', ax=axs[0])
axs[0].set_title('v61 by isfraud')
fraud_df.groupby(('v61'))['isfraud'].count().plot(kind="bar", ax=axs[1])
axs[1].set_title("v61 by isfraud")
plt.tight_layout()
plt.show()
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
sns.boxplot(x='v19', y='isfraud', data=fraud_df, orient='h', ax=axs[0])
axs[0].set_title('v19 by isfraud')
fraud_df.groupby(('v19'))['isfraud'].count().plot(kind="bar", ax=axs[1])
axs[1].set_title("v19 by isfraud")
plt.tight_layout()
plt.show()
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
sns.boxplot(x='v83', y='isfraud', data=fraud_df, orient='h', ax=axs[0])
axs[0].set_title('v83 by isfraud')
fraud_df.groupby(('v83'))['isfraud'].count().plot(kind="bar", ax=axs[1])
axs[1].set_title("v83 by isfraud")
plt.tight_layout()
plt.show()
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
sns.boxplot(x='v288', y='isfraud', data=fraud_df, orient='h', ax=axs[0])
axs[0].set_title('v288 by isfraud')
fraud_df.groupby(('v288'))['isfraud'].count().plot(kind="bar", ax=axs[1])
axs[1].set_title("v288 by isfraud")
plt.tight_layout()
plt.show()
fig, axs = plt.subplots(1, 2, figsize=(16, 6))
sns.boxplot(x='v82', y='isfraud', data=fraud_df, orient='h', ax=axs[0])
axs[0].set_title('v82 by isfraud')
fraud_df.groupby(('v82'))['isfraud'].count().plot(kind="bar", ax=axs[1])
axs[1].set_title("v82 by isfraud")
plt.tight_layout()
plt.show()
Tras analizar los boxplot y las frecuencias de estas variables y dado que no se conoce significado concreto de cada una, se ha decidido eliminar a partir del outlier que la frecuencia comienza a ser demasiado baja, ya que podrían ser datos erróneos.
fraud_df = fraud_df.loc[(fraud_df['v302'] <= 4) | (fraud_df['v302'].isna())]
fraud_df = fraud_df.loc[(fraud_df['v62'] <= 4) | (fraud_df['v62'].isna())]
fraud_df = fraud_df.loc[(fraud_df['v20'] <= 4) | (fraud_df['v20'].isna())]
fraud_df = fraud_df.loc[(fraud_df['v61'] <= 3) | (fraud_df['v61'].isna())]
fraud_df = fraud_df.loc[(fraud_df['v19'] <= 3) | (fraud_df['v19'].isna())]
fraud_df = fraud_df.loc[(fraud_df['v83'] <= 4) | (fraud_df['v83'].isna())]
fraud_df = fraud_df.loc[(fraud_df['v288'] <= 3) | (fraud_df['v288'].isna())]
fraud_df = fraud_df.loc[(fraud_df['v82'] <= 3) | (fraud_df['v82'].isna())]
A continuación voy a realizar la correlación de Pearson para descartar variables que tengan una alta relación, en caso de que existan.
Como son demasiadas variables numéricas y antes ya se ha hablado de su significado, se van a calcular las correlaciones por grupos de nombres por separado, ya que son las que es más probable que tengan relación y así se puede analizaar más detalladamente.
Por otra parte, en el caso de las columnas v se realizará por grupos de 50, dado que son demasiadas columnas y no se visualizaría correctamente.
correlacion_c = ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14']
plt.figure(figsize=(8,6))
sns.heatmap(round(fraud_df[correlacion_c].corr('pearson'),2), cmap="YlGnBu", annot=True, linewidth=.5)
plt.show()
# I use reduce_group function that I have created to decided the columns to maintain.
grps = [[1,2,4,6,7,8,10,11,12,13,14],[3],[5,9]]
reduce_group(grps, c='c')
# I drop c2, c4, c6, c7, c8, c9, c10, c11, c12, c13, c14 column.
fraud_df = fraud_df.drop(['c2', 'c4', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14'], axis=1)
Las columnas a mantener son: [1, 3, 5]
correlacion_d = ['d1', 'd2', 'd3', 'd4', 'd10', 'd11', 'd15']
plt.figure(figsize=(8,6))
sns.heatmap(round(fraud_df[correlacion_d].corr('pearson'),2), cmap="YlGnBu", annot=True, linewidth=.5)
plt.show()
La correlación más alta se da entre d1 - d2, se va a mantener d1 y eliminar d2. Tras revisar sus valores son practicamente iguales, pero cuando hay valores 0 en d1, son sustituidos por nulos en d2.
# I drop d2 column.
fraud_df = fraud_df.drop('d2', axis=1)
correlacion_v1 = [
'v2','v3','v4','v5','v6','v7','v8','v9','v10','v11','v12','v13','v15','v16','v17','v18','v19','v20','v21','v22',
'v23','v24','v25','v26','v27','v28','v29','v30','v31','v32','v33','v34'
]
plt.figure(figsize=(20,10))
sns.heatmap(round(fraud_df[correlacion_v1].corr('pearson'),2), cmap="YlGnBu", annot=True, linewidth=.5)
plt.show()
grps = [
[2,3],[4,5],[6,7],[8,9],[10,11],[12,13],[15,16,17,18,21,22,31,32,33,34],[19,20],[23,24],[25,26],[27,28],
[29,30]
]
reduce_group(grps)
Las columnas a mantener son: [3, 4, 6, 8, 11, 13, 17, 20, 23, 26, 27, 30]
correlacion_v2 = [
'v35','v36','v37','v38','v39','v40','v42','v43','v44','v45','v46','v47','v48','v49','v50','v51','v52','v53','v54',
'v55','v56','v57','v58','v59','v60', 'v61','v62','v63','v64','v66', 'v67', 'v68', 'v69','v70', 'v71','v72','v73',
'v74'
]
plt.figure(figsize=(25,15))
sns.heatmap(round(fraud_df[correlacion_v2].corr('pearson'),2), cmap="YlGnBu", annot=True, linewidth=.5)
plt.show()
grps = [[35,36],[37,38],[39,40,42,43,50,51,52,57,58,59,60,63,64,71,72,73,74],[44,45],[46,47],[48,49],[53,54],[55,56],
[61,62],[66,67],[68],[69,70]]
reduce_group(grps)
Las columnas a mantener son: [36, 37, 40, 44, 47, 48, 54, 56, 62, 67, 68, 70]
correlacion_v3 = [
'v75', 'v76','v77','v78','v79','v80','v81','v82','v83','v84','v85', 'v86','v87', 'v89', 'v90', 'v91','v92','v93',
'v94', 'v95', 'v96', 'v97', 'v98','v99','v100', 'v101', 'v102', 'v103', 'v104','v105','v106'
]
plt.figure(figsize=(20,10))
sns.heatmap(round(fraud_df[correlacion_v3].corr('pearson'),2), cmap="YlGnBu", annot=True, linewidth=.5)
plt.show()
grps = [[75,76],[77,78],[79,80,81,84,85,92,93,94],[82,83],[86,87],[89],[90,91],[95,96,97,101,102,103,105,106],[98],
[99,100],[104]]
reduce_group(grps)
Las columnas a mantener son: [76, 78, 80, 83, 86, 89, 91, 96, 98, 99, 104]
correlacion_v4 = [
'v108','v109', 'v110','v111','v112','v113','v114','v115','v116','v117','v118','v119','v120','v121','v122','v123',
'v124', 'v125','v126','v127','v128','v129','v130','v131','v132','v133','v134','v135','v136','v137'
]
plt.figure(figsize=(20,10))
sns.heatmap(round(fraud_df[correlacion_v4].corr('pearson'),2), cmap="YlGnBu", annot=True, linewidth=.5)
plt.show()
grps = [[108,109,110,114],[111,112,113],[115,116],[117,118,119],[120,122],[121],[123,125],[124],
[126,127,128,132,133,134],[129],[130,131],[135,136,137]]
reduce_group(grps)
Las columnas a mantener son: [108, 111, 115, 117, 120, 121, 123, 124, 127, 129, 130, 136]
correlacion_v5 = [
'v279','v280','v281', 'v282','v283','v284','v285','v286','v287','v288','v289','v290','v291','v292','v293','v294',
'v295','v296','v297', 'v298','v299','v300', 'v301','v302','v303','v304','v305', 'v306','v307','v308','v309','v310',
'v311','v312','v313','v314', 'v315','v316','v317','v318','v319','v320','v321'
]
plt.figure(figsize=(25,25))
sns.heatmap(round(fraud_df[correlacion_v5].corr('pearson'),2), cmap="YlGnBu", annot=True, linewidth=.5)
plt.show()
grps = [[279,280,293,294,295,296,298,299],[281],[282,283],[284],[285,287],[286],[288,289],[290,291,292],[297],
[300,301],[302,303,304],[305],[306,307,308,316,317,318],[309,311],[310,312],[313,314,315],[319,320,321]]
reduce_group(grps)
Las columnas a mantener son: [294, 281, 283, 284, 285, 286, 289, 291, 297, 301, 303, 305, 307, 309, 310, 314, 320]
v_to_drop = [
'v2', 'v5', 'v7', 'v9', 'v10', 'v12', 'v15', 'v16', 'v18', 'v19', 'v21', 'v22', 'v24', 'v25', 'v28', 'v29',
'v31', 'v32', 'v33', 'v34', 'v35', 'v38', 'v39', 'v42','v43', 'v45','v46', 'v49','v50','v51','v52','v53',
'v55', 'v57','v58','v59','v60', 'v61', 'v63','v64','v66', 'v69', 'v71','v72', 'v73','v74',
'v75', 'v77', 'v79', 'v81', 'v82','v84','v85','v87', 'v90', 'v92','v93', 'v94', 'v95', 'v97', 'v100','v101',
'v102', 'v103', 'v105','v106','v109', 'v110','v112','v113','v114', 'v116', 'v118','v119', 'v122', 'v125','v126',
'v128', 'v131','v132','v133','v134','v135','v137', 'v279','v280', 'v282', 'v287','v288', 'v290', 'v292','v293',
'v295','v296','v298','v299','v300', 'v302', 'v304','v306', 'v308', 'v311', 'v312','v313', 'v315','v316','v317',
'v318','v319', 'v321'
]
# I drop v columns.
fraud_df = fraud_df.drop(v_to_drop, axis=1)
correlacion_vfinal = [
'v3','v4','v6','v8','v11','v13','v17','v20','v23','v26','v27','v30',
'v36','v37','v40','v44','v47','v48','v54','v56','v62','v67','v68','v70',
'v76','v78','v80','v83','v86', 'v89','v91','v96','v98','v99', 'v104',
'v108','v111','v115','v117','v120','v121','v123','v124','v127','v129','v130','v136',
'v281','v283','v284','v285','v286','v289','v291','v294','v297', 'v301','v303','v305','v307','v309','v310','v314','v320'
]
plt.figure(figsize=(25,25))
sns.heatmap(round(fraud_df[correlacion_vfinal].corr('pearson'),2), cmap="YlGnBu", annot=True, linewidth=.5)
plt.show()
fraud_df.to_parquet("../data/processed/fraud_df_initial.parquet")